﻿#region License
// (c) Garry McGlennon 2009
// 
// This project is made available under the Microsoft Public License (Ms-PL)
// Code is provided as is and with no warrenty – Use at your own risk
// 
// View the project and the latest code at http://dnpframework.codeplex.com/
// 
// All other rights reserved.
// 
// blog: www.dotNetProfessional.com/blog/
#endregion
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace dnp.Framework.Web
{
    public class IISWebLogFileProvider : IWebLogFileProvider
    {
        #region IWebLogFileProvider Members

        // This regex should also work with Appache logs
        readonly string _regEx = "(?:^|[ ])([\"\\[](?:[^\"\\]]+|\"\")*[\"\\]]|[^ ]*)";
        private Dictionary<string, int> _availableFields = new Dictionary<string, int>();
        private bool _logFormatVerified;

        public LogEntry Parse(string logEntry)
        {
            // Verify this is an IIS log file
            if (!_logFormatVerified && logEntry.Contains("Microsoft Internet Information"))
            {
                _logFormatVerified = true;
                return null; // No entry to parse on this line
            }

            // Check for the field list
            if (_availableFields.Count == 0 && logEntry.StartsWith("#Fields:"))
            {
                this.ParseAvailableFields(logEntry);
                return null; // No entry to parse on this line
            }

            if (logEntry.Length == 0 || logEntry.StartsWith("#"))
                return null; // No entry to parse on this line

            // If we get this far the logEntry should be valid!
            // Verify that we've passed all the initialisation tasks before processing
            // a real log entry
            if (_logFormatVerified && _availableFields.Count != 0)
                return this.ParseLogEntry(logEntry);


            // Ok somethings not right best let the user know about it...
            if (!_logFormatVerified)
                throw new ArgumentException("Invalid format - file must be an IIS Log file.");

            if (_availableFields.Count == 0)
                throw new ArgumentException("Unable to identify the fields in this file.");

            throw new ArgumentException("Unexpected log entry format.");
        }

        #endregion

        /// <summary>
        /// Intialise the fields defined with this log file. Not all log files have the same set of fields
        /// or in the same order.
        /// </summary>
        /// <param name="logEntry"></param>
        private void ParseAvailableFields(string logEntry)
        {
            string[] fields = logEntry.Split(' ');
            for (int i = 1; i < fields.Length; i++)
            {
                _availableFields.Add(fields[i], i - 1); // Need to offset by one to eliminate the #Fields first entry
            }
        }

        /// <summary>
        /// Core routine to parse a log entry into an instance.
        /// </summary>
        /// <param name="logEntry"></param>
        /// <returns></returns>
        private LogEntry ParseLogEntry(string logEntry)
        {
            // Parse the logEntry using the RegEx
            // the regex is compiled to provide faster performance on large files
            MatchCollection matches = Regex.Matches(logEntry, _regEx, RegexOptions.Compiled);
            LogEntry e = new LogEntry();

            e.Bytes = this.ProcessIntValue("sc-bytes", matches);
            e.Bytes1 = this.ProcessIntValue("cs-bytes", matches);
            e.Computername = this.ProcessStringValue("s-computername", matches);
            e.Cookie = this.ProcessStringValue("cs(Cookie)", matches);
            e.Date = this.ProcessDateTime("date", matches);
            e.Host = this.ProcessStringValue("cs-host", matches);
            e.IpAddress = this.ProcessStringValue("c-ip", matches);
            e.Method = this.ProcessStringValue("cs-method", matches);
            e.PortNumber = this.ProcessIntValue("s-port", matches);
            e.Referrer = this.ProcessStringValue("cs(Referer)", matches);
            e.SiteName = this.ProcessStringValue("s-sitename", matches);
            e.Status = this.ProcessStringValue("sc-status", matches);
            e.SubStatus = this.ProcessStringValue("sc-substatus", matches);
            e.Time = this.ProcessDateTime("time", matches);
            //e.TimeTaken = _availableFields.ContainsKey("sc-substatus") ? matches[_availableFields["sc-substatus"]].Value : "";
            e.UriQuery = this.ProcessStringValue("cs-uri-query", matches);
            e.UriStem = this.ProcessStringValue("cs-uri-stem", matches);
            e.UserAgent = this.ProcessStringValue("cs(User-Agent)", matches);
            e.Version = this.ProcessStringValue("cs-version", matches);
            e.Win32Status = this.ProcessStringValue("sc-win32-status", matches);

            return e;
        }

        /// <summary>
        /// This routine retieves the value and makes any necessary fixes
        /// </summary>
        /// <param name="key">the column name</param>
        /// <param name="matches">the result of the regex</param>
        /// <returns></returns>
        private string ProcessStringValue(string key, MatchCollection matches)
        {
            string value;
            // As we may be missing some fields we need to test each one.
            if (_availableFields.ContainsKey(key))
            {
                value = matches[_availableFields[key]].Value;
                // IIS logs place a + for special characters or spaces, this is to 
                // convert those back to spaces (inline with other log formats)
                value = value.Replace("+", " ");

                // The log files sometimes have leading and trailing spaces
                value = value.Trim();
                return value;
            }
            else
                return "";
        }

        /// <summary>
        /// This routine retieves the value and makes any necessary fixes
        /// </summary>
        /// <param name="key">the column name</param>
        /// <param name="matches">the result of the regex</param>
        /// <returns></returns>
        private int ProcessIntValue(string key, MatchCollection matches)
        {
            if (_availableFields.ContainsKey(key))
                return Convert.ToInt32(matches[_availableFields[key]].Value);
            else
                return 0;
        }

        /// <summary>
        /// This routine retieves the value and makes any necessary fixes
        /// </summary>
        /// <param name="key">the column name</param>
        /// <param name="matches">the result of the regex</param>
        /// <returns></returns>
        private DateTime ProcessDateTime(string key, MatchCollection matches)
        {
            if (_availableFields.ContainsKey(key))
                return Convert.ToDateTime(matches[_availableFields[key]].Value);
            else
                return DateTime.MinValue;
        }
    }

}
